﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NGinnBPM.Lib.Data;
using NGinnBPM.Lib.Schema;
using NGinnBPM.Lib.Interfaces;
using Jint;
using Jint.Native;
using NLog;
using System.Collections;
using System.Reflection;

namespace NGinnBPM.Services.Scripting.JintScript
{
    /// <summary>
    /// Jint task script implementation
    /// TODO
    /// 1. Add unwrapping of return values of scripts. Auto-unwrapping does not work for jsObjects and arrays
    /// 2. Add script pre-compilation
    /// </summary>
    class JintTaskScript : ITaskScript
    {
        private JintEngine _eng;
        private ProcessDefinition _pd;
        private Task _tsk;
        private static Logger log = LogManager.GetCurrentClassLogger();
        private JintProcessScriptManager parent;

        public JintTaskScript(JintEngine eng, ProcessDefinition pd, string taskId, JintProcessScriptManager scriptMgr)
        {
            _eng = eng;
            _pd = pd;
            _tsk = pd.GetTask(taskId);
            parent = scriptMgr;
        }

        public Dictionary<string, object> SourceData
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                _eng.SetParameter("data", WrapObjectAsJsInstance(value, _eng.Global));
            }
        }

        public Runtime.ITaskContext TaskContext
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                _eng.SetParameter("ctx", value);
                _eng.SetParameter("processDef", value.ParentProcess);
                
            }
        }

        public Runtime.TaskInstance TaskInstance
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                _eng.SetParameter("taskInstance", value);
            }
        }

        private object EvalVariableBinding(VariableBinding vb)
        {
            try
            {
                if (vb.BindType != VariableBindingType.Expr) throw new Exception("Only expressions allowed");
                return EvalExpression(vb.Expression);
            }
            catch (Exception ex)
            {
                log.Error("Error evaluating Task {0} variable binding {1} (expression: {2}): {3}", _tsk.Id, vb.VariableName, vb.Expression, ex);
                throw;
            }
        }

        private object RunScript(string script)
        {
            var prg = parent.GetCompiledScript(script);
            var ret = _eng.Run(prg, false);
            log.Debug("Running {0}, return: {1}", script, ret);
            return ret;
        }

        private object EvalExpression(string expression)
        {
            string script = string.Format("return ({0});", expression);
            JsInstance ret = (JsInstance) RunScript(script);
            object rv = UnwrapJsInstance(ret);
            if (log.IsDebugEnabled)
                log.Info("Evaled {0} to {1} ({2}) and unwrapped to {3} ({4})", expression, ret, ret.GetType(), rv, rv.GetType());
            return rv;
        }

        public object GetInputParameterValue(string paramName)
        {
            VariableBinding vb = _tsk.GetInputParameterBinding(paramName);
            return EvalVariableBinding(vb);
        }

        public object GetDefaultVariableValue(string variableName)
        {
            VariableDef vd = _tsk.GetVariable(variableName);
            if (string.IsNullOrEmpty(vd.DefaultValueExpr)) throw new Exception("Variable has no default value: " + vd);
            return EvalExpression(vd.DefaultValueExpr);
        }

        public object RunScriptBlock(string blockId)
        {
            if (blockId == "BeforeEnableScript")
            {
                return EvalExpression(_tsk.BeforeEnableScript);
            }
            else if (blockId == "AfterEnableScript")
            {
                return EvalExpression(_tsk.AfterEnableScript);
            }
            else if (blockId == "BeforeCompleteScript")
            {
                return EvalExpression(_tsk.BeforeCompleteScript);
            }
            else
                return GetInputParameterValue(blockId);
        }

        public object EvalInputVariableBinding(string varName)
        {
            VariableBinding vb = _tsk.GetInputBinding(varName);
            return EvalVariableBinding(vb);
        }

        public object EvalOutputVariableBinding(string varName)
        {
            VariableBinding vb = _tsk.GetOutputBinding(varName);
            return EvalVariableBinding(vb);
        }

        public object EvalOutputParameterBinding(string varName)
        {
            VariableBinding vb = _tsk.GetOutputParameterBinding(varName);
            return EvalVariableBinding(vb);
        }

        public object EvalMultiInstanceSplitQuery()
        {
            return EvalExpression(_tsk.MultiInstanceSplitExpression);
        }

        public bool EvaluateFlowInputCondition(Flow fl)
        {
            return Convert.ToBoolean(EvalExpression(fl.InputCondition));
        }

        static JsInstance WrapObjectAsJsInstance(object v, IGlobal glob)
        {
            if (v == null)
                return new JsNull();
            else if (typeof(JsInstance).IsAssignableFrom(v.GetType()))
                return (JsInstance) v;
            else if (v is string)
                return new JsString((string)v, glob.StringClass);
            else if (v is int || v is short || v is long)
                return new JsNumber(Convert.ToInt32(v), glob.NumberClass);
            else if (v is DateTime)
                return new JsDate((DateTime)v, glob.DateClass);
            else if (v is bool)
                return new JsBoolean((bool)v, glob.BooleanClass);
            else if (v is float || v is double)
                return new JsNumber(Convert.ToDouble(v), glob.NumberClass);
            else if (v is IDictionary<string, object>)
            {
                IDictionary<string, object> vals = (IDictionary<string, object>) v;
                JsObject obj = new JsObject(glob.ObjectClass);
                foreach (string k in vals.Keys)
                    obj[k] = WrapObjectAsJsInstance(vals[k], glob);
                return obj;
            }
            else if (v is IDictionary)
            {
                IDictionary vals = (IDictionary) v;
                JsObject obj = new JsObject(glob.ObjectClass);
                foreach (string k in vals.Keys)
                    obj[k] = WrapObjectAsJsInstance(vals[k], glob);
                return obj;
            }
            else if (v is IDynData)
            {
                IDynData vals = (IDynData) v;
                JsObject obj = new JsObject(glob.ObjectClass);
                foreach (string k in vals.FieldNames)
                    obj[k] = WrapObjectAsJsInstance(vals[k], glob);
                return obj;
            }
            else if (v is IEnumerable)
            {
                JsArray arr = new JsArray(glob.ArrayClass);
                foreach(object obj in ((IEnumerable) v))
                {
                    arr.put(arr.Length, WrapObjectAsJsInstance(obj, glob));
                }
                return arr;
            }
            else
                throw new Exception("Unexpected type: " + v.GetType());
        }

        static object UnwrapJsInstance(JsInstance inst)
        {
            if (inst is JsNull || inst is JsUndefined)
                return null;
            if (inst.Value != null)
                return inst.Value;
            if (inst is JsArray)
            {
                JsArray arr = inst as JsArray;
                List<object> ret = new List<object>();

                foreach (string k in arr.GetKeys())
                {
                    int n;
                    if (Int32.TryParse(k, out n) && n == ret.Count)
                    {
                        ret.Add(UnwrapJsInstance(arr[k]));
                    }
                    else
                    {
                        log.Debug("Non-integer or non-consecutive index, will return the array as a dictionary: {0}=>{1}", k, arr[k]);
                        ret = null;
                        break;
                    }
                }
                if (ret != null) return ret;
            }
            //arrays with non-integer indexes and other JsObjects
            if (inst is JsArray || inst is JsObject)
            {
                if (inst.IsClr) throw new Exception("CLR type shouldn't happen here");
                JsObject obj = inst as JsObject;
                Dictionary<string, object> d = new Dictionary<string, object>();
                foreach (string k in obj.GetKeys())
                {
                    d[k] = UnwrapJsInstance(obj[k]);
                }
                return d;
            }
            else throw new NotImplementedException("Unwrapping not supported for " + inst.GetType());
        }



       
    }
}
